#include <assert.h>
#include <stdlib.h>
#include <string.h>

#include "debug.h"
#define verbose SOS_DEBUG_VFS
#include <sys/debug.h>
#include <sys/panic.h>

#include "frametable.h"
#include "macros.h"
#include "pagetable.h"
#include "process.h"
#include "vfs.h"

static inline int _vfs_add_mountpoint(sos_vfs *vfs, const char *mpoint,
                                      sos_mount *mount);
static inline int _vfs_resolve_mountpoint(sos_vfs *vfs, const char *path,
                                          sos_mount **mount, char *file);
static inline sos_fd *_vfs_lookup_fd(sos_pcb *pcb, int proc_fd);
static inline int _vfs_create_fd(sos_pcb *pcb, sos_fd **fd);
static inline int _vfs_delete_fd(sos_pcb *pcb, sos_fd *fd);

int sos_vfs_init(sos_vfs **vfs) {
    if (vfs == NULL) return SOS_VFS_INVAL;
    (*vfs) = (sos_vfs *) malloc(sizeof(struct sos_vfs));
    if (*vfs == NULL) return SOS_VFS_OOM;

    // XXX M5 This goes away in favour of a trie
    (*vfs)->console.path = NULL;
    (*vfs)->console.mount = NULL;

    // XXX M5 This goes
    (*vfs)->nfs.path = NULL;
    (*vfs)->nfs.mount = NULL;

    return SOS_VFS_SUCCESS;
}

int sos_vfs_mount(sos_vfs *vfs, const char *mpoint, sos_filesystem *fs,
                  const char *fs_data) {
    int err;
    sos_mount *mount;
    // Make sure we didn't receive garbage
    if (vfs == NULL || fs == NULL) return SOS_VFS_INVAL;

    // Reserve some memory for the mount.
    mount = malloc(sizeof(struct sos_mount));
    if (mount == NULL) return SOS_VFS_OOM;

    // XXX M4 We should be checking mpoint against our idea of 'valid path'
    //        For M5, a valid path / mountpoint is "/[ascii]*"
    // We don't need to check fs_data - we don't understand it anyway.
    err = (*(fs->mount))(mount, fs_data);
    if (err) {
        free(mount);
        return err;
    }

    // Add it to the VFS mount collection somehow
    err = _vfs_add_mountpoint(vfs, mpoint, mount);

    return err;
}

int sos_vfs_open(sos_pcb *pcb, sos_vfs *vfs, const char *path, fmode_t mode) {
    int err;
    sos_mount *mount;
    char file[SOS_PATH_MAX];
    sos_fd *fd;
    list args;

    if (pcb == NULL || vfs == NULL || path == NULL) {
        dprintf(3, "open: VFS uninitialised\n");
        return SOS_VFS_INVAL;
    }

    // Grab a process FD and allocate memory for bookkeeping
    err = _vfs_create_fd(pcb, &fd);
    if (err) return err;

    // Figure out the relevant mountpoint
    err = _vfs_resolve_mountpoint(vfs, path, &mount, file);
    if (err) return err;
    fd->fs = mount;

    // Call its idea of open
    err = (*(mount->open))(pcb, file, mode, fd->proc_fd, &(fd->fs_fd));

    return err;
}

int sos_vfs_close(sos_pcb *pcb, sos_vfs *vfs, int proc_fd) {
    int err;
    sos_fd *fd;

    // Check that this FD is valid
    fd = _vfs_lookup_fd(pcb, proc_fd);
    if (fd == NULL) return SOS_VFS_NOENT;

    // Hand off to the filesystem that owns this FD - ask it to close
    err = (*(fd->fs->close))(pcb, fd->fs_fd);
    if (err) return err;

    // Remove from process FD table
    return (_vfs_delete_fd(pcb, fd));
}

int sos_vfs_read(sos_pcb *pcb, sos_vfs *vfs, int proc_fd,
                 seL4_Word proc_vaddr, size_t nbyte) {
    sos_fd *fd;

    // Look up FD from PCB
    fd = _vfs_lookup_fd(pcb, proc_fd);
    if (fd == NULL) {
        sos_syscall_read_reply(pcb, SOS_VFS_NOENT, 0);
        return SOS_VFS_NOENT;
    }

    // Hand off to FS. Any asynchronous behaviour is their problem
    return (*(fd->fs->read))(pcb, fd->fs_fd, (char *) proc_vaddr,
                             nbyte);
}

int sos_vfs_write(sos_pcb *pcb, sos_vfs *vfs, int proc_fd,
                  seL4_Word proc_vaddr, size_t nbyte) {
    sos_fd *fd;

    // Look up FD from PCB
    fd = _vfs_lookup_fd(pcb, proc_fd);
    if (fd == NULL) {
        sos_syscall_write_reply(pcb, SOS_VFS_NOENT, 0);
        return SOS_VFS_NOENT;
    }

    // Hand off to FS. Any asynchronous behaviour is their problem
    return (*(fd->fs->write))(pcb, fd->fs_fd, (char *) proc_vaddr,
                              nbyte);
}

int sos_vfs_stat(sos_pcb *pcb, sos_vfs *vfs, const char *path) {
    int err;
    sos_mount *mount;
    char file[SOS_PATH_MAX];
    sos_stat_t stat_buf = { 0 };

    if (pcb == NULL || vfs == NULL || path == NULL) {
        dprintf(6, "vfs_stat: VFS uninitialised\n");
        sos_syscall_stat_reply(pcb, SOS_VFS_INVAL, stat_buf);
        return SOS_VFS_INVAL;
    }

    // Figure out the relevant mountpoint
    err = _vfs_resolve_mountpoint(vfs, path, &mount, file);
    if (err) {
        dprintf(6, "stat: could not find mountpoint\n");
        sos_syscall_stat_reply(pcb, SOS_VFS_INVAL, stat_buf);
        return err;
    }

    // Call its idea of stat
    err = (*(mount->stat))(pcb, file);
    if (err) return err;

    return SOS_VFS_SUCCESS;
}

int sos_vfs_getdirent(sos_pcb *pcb, sos_vfs *vfs, int pos, size_t max) {
    int err;
    sos_mount *mount;

    if (pcb == NULL || vfs == NULL) {
        dprintf(3, "open: VFS uninitialised\n");
        sos_syscall_getdirent_reply(pcb, NULL, 0, SOS_VFS_INVAL);
        return SOS_VFS_INVAL;
    }

    err = (*(vfs->nfs.mount->getdirent))(pcb, pos, max);
    if (err) return err;

    return SOS_VFS_SUCCESS;
}

// int sos_vfs_umount

// int sos_vfs_dispose/free

// XXX M5 Trie this.
//     We only have two fstab slot for now.
//     This is super hacky and expects a certain mount order
static inline int _vfs_add_mountpoint(sos_vfs *vfs, const char *mpoint,
                                      sos_mount *mount) {
    if (vfs == NULL || mpoint == NULL || mount == NULL) return SOS_VFS_INVAL;
    if (vfs->console.mount == NULL) {
        vfs->console.path = mpoint;
        vfs->console.mount = mount;
        return SOS_VFS_SUCCESS;
    } else if (vfs->nfs.mount == NULL) {
        vfs->nfs.path = mpoint;
        vfs->nfs.mount = mount;
        return SOS_VFS_SUCCESS;
    } else {
        return SOS_VFS_REMOUNT;
    }
}

// XXX M5 Trie this.
static inline int _vfs_resolve_mountpoint(sos_vfs *vfs, const char *path,
                                          sos_mount **mount, char *file) {

    int flen = strnlen(path, SOS_PATH_MAX);
    // Need a trie for mountpoint resolution
    if (strncmp(path, "console", 8) == 0) {
        dprintf(4, "Passing VFS op through to console...\n");
        (*mount) = vfs->console.mount;
        // Pass the remainder of the path string to the filesystem
        if (file != NULL) file = strncpy(file, path + 7, flen + 1);
    } else {
        // Assume NFS
        dprintf(4, "Passing VFS op through to NFS...\n");
        if (vfs->nfs.mount == NULL) return SOS_VFS_NOENT;
        (*mount) = vfs->nfs.mount;
        // Pass the whole path string to the filesystem
        if (file != NULL) file = strncpy(file, path, flen + 1);
    }
    return SOS_VFS_SUCCESS;
}

static inline int _vfs_create_fd(sos_pcb *pcb, sos_fd **fd) {
    sos_fd *found;
    int cur_fd = 0;

    while (cur_fd < SOS_MAX_FD) {
        // Lowest-available FD for this process
        found = NULL;
        HASH_FIND_INT(pcb->fd_table, &cur_fd, found);
        if (found == NULL) {
            dprintf(6, "vfs_create_fd: selected %d\n", cur_fd);
            break;
        }
        cur_fd++;
    }

    if (found != NULL) return SOS_VFS_INVAL;

    // FDs are stored in the fd_table - we have to manage the memory here
    (*fd) = malloc(sizeof(struct sos_fd));
    if ((*fd) == NULL) return SOS_VFS_OOM;

    // Grab a process-facing FD
    (*fd)->proc_fd = cur_fd;

    // Add the structure to the process FD table
    HASH_ADD_INT(pcb->fd_table, proc_fd, (*fd));

    return SOS_VFS_SUCCESS;
}

static inline sos_fd *_vfs_lookup_fd(sos_pcb *pcb, int proc_fd) {
    sos_fd *fd;
    HASH_FIND_INT(pcb->fd_table, &proc_fd, fd);
    return fd;
}

static inline int _vfs_delete_fd(sos_pcb *pcb, sos_fd *fd) {
    HASH_DEL(pcb->fd_table, fd);
    free(fd);
    return SOS_VFS_SUCCESS;
}
